using System;
using System.Collections.Generic;
using System.Text;
using CodePlex.Resourceful;
using CodePlex.Resourceful.Amazon.S3;

namespace CodePlex.SpaceBlock.FS
{
    public class S3FileSystem : BaseFileSystem, IFileSystem, ISupportsTransferWithMetadata, ISupportsACL, ISupportsMove
    {
        private readonly S3Connection _connection;
        private readonly S3DefaultTransferACL _defaultTransferACL;


        public S3FileSystem(S3Connection connection)
            : this(connection,S3DefaultTransferACL.Private)
        {

        }
        public S3FileSystem(S3Connection connection, S3DefaultTransferACL defaultTransferACL)
        {
            _connection = connection;
            _defaultTransferACL = defaultTransferACL;
        }

        public Tracer Tracer
        {
            get
            {
                return _connection.Tracer;
            }
        }

        public S3Connection S3Connection
        {
            get
            {
                return _connection;
            }
        }
        
        public string Caption
        {
            get 
            {
                return string.Format("Amazon S3 File System - {0}", this.ShortCaption); 
            }
        }

        public string ShortCaption
        {
            get
            {
                return _connection.Owner.Name + (_connection.IsSSL ? " (ssl)" : "");
            }
        }

        public S3DefaultTransferACL DefaultTransferACL
        {
            get
            {
                return _defaultTransferACL;
            }
        }
       
        public void EnsureFolderExists(Path path)
        {
            this.CheckNull(path);
            this.CheckFolder(path);

            if (path.Level == 0)
            {
                return; // root always exists
            }


            if (this.Exists(path)) 
            {
                return;
            }


            if (path.Level == 1)
            {
                // create bucket

                _connection.CreateBucket(path.Name);
                return;
               
               
            } 

            // ensure bucket exists
            this.EnsureFolderExists(this.GetBucketPath(path));


            // ensure slash key exists (as a last resort)
            // upload a zero-byte file
            _connection.PutObjectAsBytes(this.GetBucketPath(path).Name, this.GetPrefix(path), new NameValuePairs(),null, new byte[0]);

          
        }


        public bool Exists(Path path)
        {
            this.CheckNull(path);

            if (path.Level == 0)
            {
                return true; // root always exists
            }

            if (path.IsFolder)
            {
                if (path.Level == 1)
                {
                    // make sure bucket exists
                    foreach (Bucket bucket in _connection.ListBuckets().Buckets)
                    {
                        if (bucket.Name == path.Name)
                        {
                            return true;
                        }
                    }
                    return false;

                }
                else
                {
                    return (_connection.ListBucket(this.GetBucketPath(path).Name, this.GetPrefix(path)).Count > 0);
                }
            }
            else
            {

                if (path.Level == 1)
                {
                    throw CodePlexException.Format("Invalid file path: {0}.", path);
                }


                // file exists?
                string prefix = this.GetPrefix(path);
                foreach(Key key in _connection.ListBucket(this.GetBucketPath(path).Name, prefix)) 
                {
                    if (key.Name == prefix)
                    {
                        return true;
                    }
                }
                return false;
              
            }
        }

        public void EnsureFileExists(Path path)
        {

            this.CheckNull(path);
            this.CheckFile(path);

            if (path.Level < 2)
            {
                throw CodePlexException.Format("Cannot create file '{0}'.", path);
            }

            if (this.Exists(path))
            {
                return;
            }

            // upload a zero-byte file
            _connection.PutObjectAsBytes(this.GetBucketPath(path).Name, this.GetPrefix(path), new NameValuePairs(), null, new byte[0]);


        }


        public void Move(Path source, Path destination)
        {
            // TODO only file sources supported right now

            this.CheckNull(source);
            this.CheckFile(source);

            this.CheckNull(destination);
            if (destination.IsFolder)
            {
                destination = destination.Combine(source.Name);
            }
            this.CheckFile(destination);

            string sourceBucket = this.GetBucketPath(source).Name;
            string sourceKey = this.GetPrefix(source);
            string destBucket = this.GetBucketPath(destination).Name;
            string destKey = this.GetPrefix(destination);


            // copy contents
            _connection.CopyObject(sourceBucket, sourceKey, destBucket, destKey);

            // copy acl
            ACL sourceACL = _connection.GetACL(sourceBucket, sourceKey);
            _connection.SetACL(destBucket, destKey, sourceACL);

            // remove source object
            _connection.DeleteObject(sourceBucket, sourceKey);

        }



        public void Delete(Path path)
        {
            this.Delete(path, true);
        }
        public void Delete(Path path, bool isLastInBatch)
        {
            this.CheckNull(path);

            if (path.Level < 2 && !(path.IsFolder && path.Level == 1))
            {
                throw CodePlexException.Format("Cannot delete {1} '{0}'.", path, (path.IsFolder ? "folder" : "file"));
            }

            string bucketName = this.GetBucketPath(path).Name;

            if (path.IsFolder)
            {
                // delete all sub keys with the prefix
                foreach (Key key in _connection.ListBucket(bucketName, this.GetPrefix(path)))
                {
                    _connection.DeleteObject(bucketName, key.Name);
                }

                if (path.Level == 1 )
                {
                    _connection.DeleteBucket(path.Name);
                }
            }
            else
            {
                _connection.DeleteObject(bucketName, this.GetPrefix(path));

                if (isLastInBatch)
                {
                    // may have just deleted the last file in the folder!
                    this.EnsureFolderExists(path.Parent);
                }
            }
        }

        public ReadOnlyList<IPathItem> GetItems(Path path)
        {
            return this.InternalGetItems(path);
        }

        public ReadOnlyList<IFolderItem> GetFolders(Path path)
        {
            this.CheckNull(path);
            this.CheckFolder(path);

            List<IFolderItem> rt = new List<IFolderItem>();

            foreach (IPathItem item in this.GetItems(path))
            {
                IFolderItem folder = item as IFolderItem;

                if (folder != null)
                {
                    rt.Add(folder);
                }
            }
            return new ReadOnlyList<IFolderItem>( rt);
        }

        public ReadOnlyList<IFileItem> GetFiles(Path path)
        {
            this.CheckNull(path);
            this.CheckFolder(path);

            List<IFileItem> rt = new List<IFileItem>();

            foreach (IPathItem item in this.GetItems(path))
            {
                IFileItem file = item as IFileItem;

                if (file != null)
                {
                    rt.Add(file);
                }
            }
            return new ReadOnlyList<IFileItem>(rt);
        }

        public IPathItem GetItem(Path path)
        {
            this.CheckNull(path);

            if (!this.Exists(path))
            {
                return null;
            }

            if (!path.IsFolder)
            {
                // more efficient call for a single file
                return this.InternalGetItem(path);
            }

            return this.GetItems(path.Parent).Find(delegate(IPathItem item) {
                return item.Path.Equals( path);
            });

        }


        public void Transfer(IFileItem source, Path destination, Action<long> bytesTransferred)
        {
            this.Transfer(source, destination, null, bytesTransferred);
        }
        public void Transfer(IFileItem source, Path destination, NameValuePairs headers, Action<long> bytesTransferred)
        {
            this.CheckNull(destination);
            this.CheckFile(destination);

            headers = headers ?? new NameValuePairs();

            _connection.PutObjectAsStream(
                    this.GetBucketPath(destination).Name,
                    this.GetPrefix(destination),
                    headers,
                    bytesTransferred,
                    source.GetInputStream,
                    source.Size
                    );
        }

        public Metadata GetMetadata(Path path)
        {
            Metadata rt = new Metadata();

            this.AddMetadata(rt, BaseMetadataKeys.Path, path.ToString());
            
            if (path.Level == 0)
            {
                this.AddMetadata(rt, BaseMetadataKeys.Type, "File System");
                this.AddMetadata(rt, S3MD.AccessKeyID, this.S3Connection.AccessKeyID);
                return rt;
            }


            string bucketName = this.GetBucketPath(path).Name;
            string key = this.GetPrefix(path);
            Datacenter datacenter = this.S3Connection.GetDatacenter(bucketName);

            this.AddMetadata(rt, S3MD.Bucket, string.Format("{0} ({1})", bucketName, datacenter));
           
            if (path.Level == 1)
            {
                this.AddMetadata(rt, BaseMetadataKeys.Type, "Virtual Folder");
                this.AddMetadata(rt, S3MD.Key, key);
                
                Bucket bucket = this.S3Connection.ListBuckets().Buckets[bucketName];

                if (bucket != null)
                {
                    this.AddMetadata(rt, S3MD.CreationDate, bucket.CreationDate.ToString());

                    // show bucket logging status as category
                    BucketLoggingStatus status = this.S3Connection.GetLoggingStatus(bucketName);

                    this.AddMetadata(rt, S3MD.LoggingEnabled, status.IsEnabled.ToString());
                    this.AddMetadata(rt, S3MD.TargetBucket, status.TargetBucket);
                    this.AddMetadata(rt, S3MD.TargetPrefix, status.TargetPrefix);

                }

                return rt;

            }

            if (path.IsFolder)
            {
                this.AddMetadata(rt, BaseMetadataKeys.Type, "Virtual Folder");
                this.AddMetadata(rt, S3MD.Prefix, key);
            } 
            else 
            {
                this.AddMetadata(rt, BaseMetadataKeys.Type, "Virtual File");
                this.AddMetadata(rt, S3MD.Key, key);

                NameValuePairs headers = _connection.GetObjectHeaders(bucketName, key);

                foreach (string header in headers.Keys)
                {
                    if (!Utils.In(header, S3Constants.Headers.AmazonRequestID, S3Constants.Headers.AmazonRequestID2))
                    {
                        string value = headers[header];
                        if (header == HttpHeader.ContentLength)
                        {
                            value = this.ComputeSizeString(long.Parse(value));
                        }
                        this.AddMetadata(rt, header, value);
                    }
                }
            }
           
            return rt;
        }

        private static class S3MD
        {
            public static readonly string AccessKeyID = "AccessKeyID";
            public static readonly string Bucket = "(Bucket)";
            public static readonly string Key = "(Key)";
            public static readonly string Prefix = "(Prefix)";
            public static readonly string CreationDate = "CreationDate";

            public static readonly string LoggingEnabled = "LoggingEnabled";
            public static readonly string TargetBucket = "TargetBucket";
            public static readonly string TargetPrefix = "TargetPrefix";
        }

        public ACL GetACL(Path path)
        {
            if (!path.IsFolder || path.Level == 1)
            {
                return _connection.GetACL(this.GetBucketPath(path).Name, this.GetPrefix(path));
            }
            else
            {
                return _connection.NewACL();
            }
        }

        public void SetACL(Path path, ACL acl)
        {
            if (!path.IsFolder || path.Level == 1)
            {
                 _connection.SetACL(this.GetBucketPath(path).Name, this.GetPrefix(path),acl);
            }
        }

        public ReadOnlyList<ACLPrincipal> GetPrincipals()
        {
            return _connection.GetPrincipals();
        }

        public ReadOnlyList<string> GetPermissions()
        {
            List<string> rt = new List<string>();

            rt.Add(S3Constants.Permissions.Read);
            rt.Add(S3Constants.Permissions.Write);
            rt.Add(S3Constants.Permissions.ReadAcp);
            rt.Add(S3Constants.Permissions.WriteAcp);
            rt.Add(S3Constants.Permissions.FullControl);

            return new ReadOnlyList<string>(rt);
        }

        public string GetSignedUrl(RequestStyle style, Path filePath, TimeSpan expirationTimeFromNow)
        {
            
            string url =  _connection.GetSignedUrl(
                style,
                HttpMethod.Get,
                this.GetBucketPath(filePath).Name,
                this.GetPrefix(filePath),
                expirationTimeFromNow);

            return Httpsify(url);
        }


        public Datacenter GetDatacenter(Path selectedFile)
        {
            return _connection.GetDatacenter(this.GetBucketPath(selectedFile).Name);
        }

        public string GetPublicUrl(RequestStyle style, Path selectedFile)
        {

            string url = _connection.GetSignedUrl(
                style,
                HttpMethod.Get,
                this.GetBucketPath(selectedFile).Name,
                this.GetPrefix(selectedFile),
                TimeSpan.Zero);

            return Httpsify(url.Substring(0, url.IndexOf('?')));


        }

        private string Httpsify(string url)
        {
            if (url.StartsWith("http://"))
            {
                return "https://" + url.Substring("http://".Length);
            }
            return url;
        }

        public Tuple<string,string> ToBucketKey(Path filePath)
        {
            return new Tuple<string, string>(
                this.GetBucketPath(filePath).Name,
                this.GetPrefix(filePath)
            );
        }















        private IFileItem InternalGetItem(Path filePath)
        {
            this.CheckNull(filePath);
            this.CheckFile(filePath);

            string prefix = this.GetPrefix(filePath);
            ReadOnlyList<Key> keys = _connection.ListBucket(this.GetBucketPath(filePath).Name, prefix);

            foreach (Key key in keys)
            {
                if (key.Name == prefix)
                {
                   return new FileItem(this, filePath, key);
                }
            }

            return null;

        }



        private void SortPathItems(List<IPathItem> items)
        {
            items.Sort(delegate(IPathItem lhs, IPathItem rhs)
            {
                return lhs.Path.ToString().CompareTo(rhs.Path.ToString());
            });
        }

        private ReadOnlyList<IPathItem> InternalGetItems(Path folderPath)
        {
            this.CheckNull(folderPath);
            this.CheckFolder(folderPath);

            List<IPathItem> rt = new List<IPathItem>();


            // list buckets
            if (folderPath.Level == 0)
            {
                foreach (Bucket bucket in _connection.ListBuckets().Buckets)
                {
                    FolderItem folderItem = new FolderItem(this, folderPath.Combine(bucket.Name + Path.SeparatorChar), bucket.CreationDate);
                    rt.Add(folderItem);
                }
                SortPathItems(rt);
                return new ReadOnlyList<IPathItem>(rt);
            }


            string prefix = this.GetPrefix(folderPath);

            ListBucketResult lbr = _connection.ListBucket(this.GetBucketPath(folderPath).Name, prefix, Path.Separator);

            // new prefix-based folder listing

            // common-prefixes -> folders
            foreach (string commonPrefix in lbr.CommonPrefixes)
            {
                string key = commonPrefix.Substring(prefix.Length);

                FolderItem folderItem = new FolderItem(this, folderPath.Combine(key), null);
               
                rt.Add(folderItem);
            }


            // keys -> files  (except for "slash keys")
            foreach (Key key in lbr.Keys)
            {
                string keyname = key.Name.Substring(prefix.Length);

                int slashIndex = keyname.IndexOf(Path.SeparatorChar);

                if (slashIndex == -1)
                {
                    if (keyname != "" || prefix.Length == 0)  // we hit the slash key
                    {
                        Path filePath = folderPath.Combine(keyname);
                        rt.Add(new FileItem(this, filePath, key));
                    }
                }

            }
            SortPathItems(rt);
            return new ReadOnlyList<IPathItem>(rt);

        }

        override protected string GetMetadataCategory(string key)
        {

            if (Utils.In(key,
                HttpHeader.ContentRange,
                HttpHeader.ETag,
                HttpHeader.ContentType,
                S3Constants.Headers.Server,
                HttpHeader.LastModified,
                HttpHeader.Date,
                HttpHeader.ContentLength,
                HttpHeader.TransferEncoding,
                S3Constants.Headers.ContentDisposition,
                S3Constants.Headers.AmazonMissingMetadata,

                S3MD.AccessKeyID,
                S3MD.Bucket,
                S3MD.Key,
                S3MD.Prefix,
                S3MD.CreationDate

                ))
            {
                return "Amazon";
            }


            if (Utils.In(key,
                S3MD.LoggingEnabled,
                S3MD.TargetBucket,
                S3MD.TargetPrefix
                ))
            {
                return "Bucket Logging Status";
            }

           
            if (MonoCompat124.StartsWith(key, S3Constants.Headers.AmazonUserMetadataPrefix, StringComparison.InvariantCultureIgnoreCase))
            {
                return "User Metadata";
            }

            return base.GetMetadataCategory(key);
        }

        override protected string GetMetadataDescription(string key, string category)
        {
            
            NameValuePairs descriptions = new NameValuePairs();

            descriptions[S3MD.AccessKeyID] = "The Access Key ID uniquely identifies an AWS account. You include it in AWS service requests to identify yourself as the sender of the request.";
            descriptions[S3MD.Bucket] = "A bucket is simply a container for objects stored in Amazon S3. Every object is contained within a bucket. Buckets serve several purposes: they organize the Amazon S3 namespace at the highest level, they identify the account responsible for storage and data transfer charges, they play a role in access control, and they serve as the unit of aggregation for usage reporting.";
            descriptions[S3MD.Key] = "A key is the unique identifier for an object within a bucket. Every object in a bucket has exactly one key. Since a bucket and key together uniquely identify each object, Amazon S3 can be thought of as a basic data map between \"bucket + key\" and the object itself.";
            descriptions[S3MD.Prefix] = "Limits the response to keys which begin with the indicated prefix. You can use prefixes to separate a bucket into different sets of keys in a way similar to how a file system uses folders.";
            descriptions[S3MD.CreationDate] = "The time that the bucket was created.";

            descriptions[S3MD.LoggingEnabled] = "Whether or not server access logging is enabled for the bucket.";
            descriptions[S3MD.TargetBucket] = "The bucket where server access logs will be delivered to. You can have your logs delivered to any bucket that you own, including the same bucket that is being logged. You can also configure multiple buckets to deliver their logs to the same target bucket. In this case you should choose a different TargetPrefix for each source bucket so that the delivered log files can be distinguished by key.";
            descriptions[S3MD.TargetPrefix] = "A prefix for the keys that the delivered log files will be stored under.";



            descriptions[S3Constants.Headers.AmazonRequestID] = "This is a unique id assigned to each request by the system. In the unlikely event that you have problems with S3, Amazon can use this to help troubleshoot the problem.";
            descriptions[S3Constants.Headers.AmazonRequestID2] = "This is a special token that will help Amazon troubleshoot problems.";
            descriptions[S3Constants.Headers.AmazonMissingMetadata] = "This is set to the number of metadata entries not returned in x-amz-meta headers. This can happen if you create metadata using an API like SOAP that supports more flexible metadata than the REST API. For example, using SOAP, you can create metadata whose values are not legal HTTP headers.";
            

            return descriptions.GetIfExists(key) ?? base.GetMetadataDescription(key,category);
        }

        private Path GetBucketPath(Path path)
        {
            while (path.Level > 1)
            {
                path = path.Parent;
            }
            return path;
        }


        private string GetPrefix(Path path)
        {
            string rt =  path.ToString().Substring(this.GetBucketPath(path).ToString().Length);

            return rt;
        }







        private class FileItem : IFileItem
        {
            private readonly Path _path;
            private readonly Key _key;
            private readonly S3FileSystem _fileSystem;

            public FileItem(S3FileSystem fileSystem, Path path, Key key)
            {
                _fileSystem = fileSystem;
                _path = path;
                _key = key;
            }
            public long Size
            {
                get 
                { 
                    return _key.Size; 
                }
            }

            public IFileSystem FileSystem
            {
                get
                {
                    return _fileSystem;
                }
            }


            public Path Path
            {
                get 
                { 
                    return _path; 
                }
            }





            public DateTime? LastModified
            {
                get 
                { 
                    return _key.LastModified; 
                }
            }


            public System.IO.Stream GetInputStream()
            {
                return _fileSystem.S3Connection.GetObjectAsStream(
                    _fileSystem.GetBucketPath(_path).Name, 
                    _fileSystem.GetPrefix(_path),
                    false);
            }


            public byte[] QuickMD5
            {
                get 
                {
                    return Utils.FromBase16(
                        _fileSystem.S3Connection.GetObjectHeaders(
                            _fileSystem.GetBucketPath(_path).Name,
                            _fileSystem.GetPrefix(_path))[HttpHeader.ETag].Trim('"')
                        );
                }
            }

         
        }


        //private class FolderItem : IFolderItem
        //{
        //    private readonly IFileSystem _fileSystem;
        //    private readonly Path _path;
        //    private readonly DateTime? _lastModified;

        //    public FolderItem(IFileSystem fileSystem, Path path, DateTime? lastModified)
        //    {
        //        _fileSystem = fileSystem;
        //        _path = path;
        //        _lastModified = lastModified;
        //    }

        //    public IFileSystem FileSystem
        //    {
        //        get
        //        {
        //            return _fileSystem;
        //        }

        //    }
        //    public Path Path
        //    {
        //        get 
        //        { 
        //            return _path; 
        //        }
        //    }

        //    public DateTime? LastModified
        //    {
        //        get 
        //        { 
        //            return _lastModified; 
        //        }
        //    }


        //}







    }
}
